React hooks
#React
勉強用リポジトリ:fuurin/nextjs-hooks
hooksとは
このあたりをさらっと読んでみる
フックの導入 – React
フック早わかり – React
5分でわかるReact Hooks - Qiita
hooksはReactのクラスコンポーネントを関数コンポーネントで簡潔に記述できるようにしたもの。
React.Componentを継承してクラスを作らなくてもReactの機能を使える
useStateなどReactが用意してくれるhookもあるが、カスタムhookを自作することもでき、他の人が作ったhookも使える
代表的なhooks
useState: クラスコンポーネントに頼らなくてもstateを持つことができる。
useEffect: レンダリング完了後に実行する。componentDidMountやcomponentDidUpdateに相当。
useContext: Contextコンポーネント下ではProp Drillingせずに好きな階層で親から渡された変数にアクセスできる
注意事項
フックはクラスの内部では動作しないので注意
ループの中で使っちゃダメらしい。hookのlinterプラグインでその辺教えてくれるらしい
チュートリアルの準備
Reactの環境構築しんどいのでNext.jsでやっていく。
$ npm init next-app nextjs-hooks --example "https://github.com/zeit/next-learn-starter/tree/master/learn-starter"
このテンプレートでプロジェクトを作成。他にも色々
$ cd nextjs-hooks
$ npm run dev もしくは yarn dev
useState
React hooksを基礎から理解する (useState編) - Qiita
stateと名前は付いているが、このコンポーネントの中でしか参照/変更できない(ローカル)。(Reduxはグローバル)
ベストな手法は? Reactのステート管理方法まとめ - ICS MEDIA
ちなみに、Svelteのようにvar count = 0を定義し、ボタンが押された時にcount = count + 1みたいなことをしても動かない。そのため、リアクティブな動作を実現するにはこのuseStateが必要。
すると、Vueで言うところのprops的なものをめんどくさくしただけのものに見える。
もうVueでよくね?とも思うが、テスト可能性はuseStateの方が良いんだろうか...?
code: pages/index.js
import { useState } from 'react'
export default function Home() {
const initialCount = Math.floor(Math.random() * 10) + 1 // 初期値: 1~10
// const state, stateを変更する関数 = useState(初期値)
const count, setCount = useState(initialCount)
const open, setOpen = useState(true)
const toggle = () => setOpen(!open)
return <><Styles/>
<button onClick={toggle}>{open ? 'close' : 'open'}</button>
<div class={open ? 'opened' : 'closed'}>
<p>現在の数字は{count}です</p>
<button onClick={() => setCount(prevState => prevState + 1)}>+ 1</button>
<button onClick={() => setCount(count - 1)}>- 1</button>
<button onClick={() => setCount(0)}>0</button>
<button onClick={() => setCount(initialState)}>最初の数値に戻す</button>
</div>
</>
}
const Styles = () => {
return <style jsx>{`
.closed {
display: none;
}
.opened {
display: block;
}
`}</style>
}
注意が必要なのは、useStateで作ったstateに変化があった時、useStateをしたコンポーネント以下(同列も含む)の全てのコンポーネントに再レンダーがかかるということ。
ボタン1が押されたときはボタン1で変化するコンポーネントだけを再レンダーしてほしい時には、後述のReact.memoやuseCallbackを利用する。
useEffect
React hooksを基礎から理解する (useEffect編) - Qiita
関数の実行タイミングをReactのレンダリング後まで遅らせるhook
クラスコンポーネントでのライフサイクルメソッドに相当
componentDidMount
componentDidUpdate
componentWillUnmount
Material-UIを導入 Reactの勉強#600d7a51c9e8790000169157
code: pages/index.js
import {useState, useEffect} from 'react'
import ButtonGroup from '@material-ui/core/ButtonGroup'
import Button from '@material-ui/core/Button'
import Input from '@material-ui/core/Input';
export default function Home() {
const count, setCount = useState(0)
const name, setName = useState('')
// 第2引数の配列に渡したstate(count)が変更された時に限りuseEffectを実行
// 第2引数を設定しなければ、同ファイル内の関係ないもの(name)が再レンダーされた時にも実行されてしまい非効率。
// 第2引数に[]を設定すると、初回のみ実行される。
// useEffectは複数回実行するとそれぞれ有効になる。上書きで前に設定したものが無効になることはない。
useEffect(() => {
// ボタンクリックの度にタブのタイトルが変わる
document.title =${count}回クリックされました
console.log('useEffect実行')
}, count)
return <>
<p>{${count}回クリックされました}</p>
<ButtonGroup color="primary" aria-label="outlined primary button group">
<Button onClick={() => setCount((prev) => prev + 1)}>ボタン</Button>
<Button onClick={() => setCount(0)}>リセット</Button>
</ButtonGroup>
<p>名前: {name}</p>
<Input value={name} onChange={ (e) => { setName(e.target.value) } }/>
</>
}
イベントリスナの削除、タイマーのキャンセルなどの「クリーンアップ関数」をreturnすることで、2度目以降のレンダリング時に前回の副作用を消してしまうことができる
code: cleanup.js
useEffect(() => {
elm.addEventListener('click', () => {})
// returned function will be called on component unmount
return () => {
elm.removeEventListener('click', () => {})
}
}, [])
useContext
React hooksを基礎から理解する (useContext編) - Qiita
Reactコンポーネントのツリーに対して「グローバル」とみなすデータを利用できる
コンポーネントの再利用をより難しくする為、慎重に利用する
使い方
createContextでContextを作成
Context.Providerタグでツリーを開始し、valueにツリーへ渡したい値を設定
useContextでContextからvalueを受け取る
index > context_a > context_b > context_cというツリーを作ってみる
code: pages/index.js
import {createContext, useState} from 'react'
import ContextA from '../components/context_a'
export const UserContext = createContext()
export const HobbyContext = createContext()
export default function Home() {
const user, setUser = useState({ name: 'fuurin', age: 25 })
const hobby, setHobby = useState('料理')
return <>
<UserContext.Provider value={user, setUser}>
<HobbyContext.Provider value={hobby, setHobby}>
<ContextA/>
</HobbyContext.Provider>
</UserContext.Provider>
</>
}
code: components/context_a.js
import ContextB from './context_b'
export default function ContextA() {
return <ContextB/>
}
code: components/context_b.js
import ContextC from './context_c'
import { useContext } from 'react'
import { UserContext, HobbyContext } from '../pages/index'
export default function ContextB() {
const user, setUser = useContext(UserContext)
const hobby, setHobby = useContext(HobbyContext)
const nextYear = () => {
setUser({...user, age: user.age + 1})
if(user.age + 1 >= 30) setHobby('子育て')
}
return <>
<ContextC/>
<button onClick={nextYear}>1年後</button>
</>
}
code: components/context_c.js
import { useContext } from 'react'
import { UserContext, HobbyContext } from '../pages/index'
export default function ContextC() {
const user, setUser = useContext(UserContext)
const hobby, setHobby = useContext(HobbyContext)
return <p>{user.name}({user.age}): 趣味は{hobby}です。</p>
}
useReducer
React hooksを基礎から理解する (useReducer編) - Qiita
reducerによって複数stateを扱ったり特定の変更のみを許すようにできるhook。useStateを内部で利用している。
code: reducer.js
const state, dispatch = useReducer(reducer,'初期値')
reducer: stateを変更するための関数
dispatch(action): {type: 'increment', payload: 0}のようなactionオブジェクトを渡し、reducerを実行する
code: pages/index.js
import { useReducer } from 'react'
import Button from '@material-ui/core/Button';
import ButtonGroup from '@material-ui/core/ButtonGroup';
const initialCounts = { first: 0, second: 100 }
const reducer = (counts, action)=> {
switch (action.type){
case 'increment1': return {...counts, first: counts.first + action.value}
case 'decrement1': return {...counts, first: counts.first - action.value}
case 'increment2': return {...counts, second: counts.second + action.value}
case 'decrement2': return {...counts, second: counts.second - action.value}
case 'reset1': return {...counts, first: initialCounts.first}
case 'reset2': return {...counts, second: initialCounts.second}
default: return counts
}
}
export default function Counters() {
const counts, dispatch = useReducer(reducer, initialCounts)
return <>
<h2>カウント1:{counts.first}</h2>
<ButtonGroup color="primary" aria-label="outlined primary button group">
<Button onClick={()=>dispatch({type: 'increment1', value: 1})}>increment</Button>
<Button onClick={()=>dispatch({type: 'decrement1', value: 1})}>decrement</Button>
<Button onClick={()=>dispatch({type: 'reset1'})}>reset</Button>
</ButtonGroup>
<h2>カウント2:{counts.second}</h2>
<ButtonGroup color="secondary" aria-label="outlined primary button group">
<Button onClick={()=>dispatch({type: 'increment2', value: 100})}>increment</Button>
<Button onClick={()=>dispatch({type: 'decrement2', value: 100})}>decrement</Button>
<Button onClick={()=>dispatch({type: 'reset2'})}>reset</Button>
</ButtonGroup>
</>
}
axiosと合わせてレスポンスステータスに応じた処理を実装できたりする
React.memoとuseCallback
React hooksを基礎から理解する (useCallback編) と React.memo - Qiita
前提として、useStateで作ったstateに変化があった時、そのコンポーネント以下のコンポーネント全てが再描画される。
例えば、次のコードではいずれかのカウントアップボタンを押すと5つの子コンポーネント全てが再描画されてしまう。
code: index.js
import React, {useState} from 'react'
const Title = () => {
console.log('Title component')
return <h2>useCallBackTest vol.1</h2>
}
const Button = ({handleClick,value}) => {
console.log('Button child component', value)
return <button type="button" onClick={handleClick}>{value}</button>
}
const Count = ({text, countState}) => {
console.log('Count child component', text)
return <p>{text}:{countState}</p>
}
export default function Counter() {
const firstCountState, setFirstCountState = useState(0)
const secondCountState, setSecondCountState = useState(10)
const incrementFirstCounter = () => setFirstCountState(firstCountState + 1)
const incrementSecondCounter = () => setSecondCountState(secondCountState + 10)
return <>
<Title/>
<Count text="+ 1 ボタン" countState={firstCountState}/>
<Count text="+ 10 ボタン" countState={secondCountState}/>
<Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
<Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
</>
}
React.memo: コンポーネントに渡されるPropsに変化があったときだけ再描画を行うようにする機能。
code: index.js
// 3種類の子コンポーネントにReact.memoを適用
const Title = React.memo(() => {
console.log('Title component'
return <h2>useCallBackTest vol.1</h2>
})
const Button = React.memo(({handleClick,value}) => {
console.log('Button child component', value)
return <button type="button" onClick={handleClick}>{value}</button>
})
const Count = React.memo(({text, countState}) => {
console.log('Count child component', text)
return <p>{text}:{countState}</p>
})
これでTitleと、押した方のボタンと関連がない方のCountはボタンを押した時再描画されなくなった。
なお、コンポーネントに渡されるPropsに変化があったかを判定する部分をカスタマイズできる。
code: memo.js
const Component = React.memo(
() => { return <HogeComponent> },
(prevProps, nextProps) => {
// 更新前後でコンポーネントへ渡されたProps(value, countStateなど)を元に等価性を判定
// trueを返せば同じと判定し、再描画しない。falseを返せば違うと判定し、再描画する。
// この第2引数に関数が渡されていなければ、propsのshallow compareで判定する。
}
)
しかし、表示が変化しないはずのButtonはこの状態では再描画されてしまう。
これは、Buttonに渡すhandleClickがstateを使っており、いずれかのstateが変化する度にButtonの持つhandleClickコールバック関数が新しいものと認識され、Buttonも新しいコンポーネントとして再描画されてしまうため。
useCallback:「第2引数の配列中のどれかが更新された時にだけ第1引数のコールバック関数を更新」するメモ化を行う
code: index.js
import React, {useState, useCallback} from 'react'
...
// コールバック関数をメモ化するuseCallbackを2つの関数に適用
const incrementFirstCounter = useCallback(
() => setFirstCountState(firstCountState + 1),
firstCountState
)
const incrementSecondCounter = useCallback(
() => setSecondCountState(secondCountState + 10),
secondCountState
)
これで押した方のButtonのみが更新される。
useMemo
React hooksを基礎から理解する (useMemo編) - Qiita
useCallbackは関数自体をメモ化するが、useMemoは関数の結果を保持してメモ化する。
以下のコードでは、increment2を押すとcount2の2乗の値を表示するが、increment1を押した時にも計算が走ってしまう
code: index.js
import React, {useState} from 'react'
export default function UseMemo() {
const count1, setCount1 = useState(0)
const count2, setCount2 = useState(0)
const increment1 = () => setCount1(count1 + 1)
const increment2 = () => setCount2(count2 + 1)
const square = () => {
console.log('execute a heavy function square(no memo)')
let i = 0
while (i < 2000000000) i++
return count2 * count2
}
return <>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
<div>square of count2: {square()}</div>
<button onClick={increment1}>increment1</button>
<button onClick={increment2}>increment2</button>
</>
}
useMemoで関数をラップすると第2引数の値に変化がない限り、保存されている入力値に対する結果を返す
code: index.js
import React, {useMemo, useState} from 'react'
export default function UseMemo() {
const count1, setCount1 = useState(0)
const count2, setCount2 = useState(0)
const increment1 = () => setCount1(count1 + 1)
const increment2 = () => setCount2(count2 + 1)
const square = useMemo(() => {
console.log('execute a heavy function square(no memo)')
let i = 0
while (i < 2000000000) i++
return count2 * count2
}, count2)
return <>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
{/* useMemoでラップすると square() から square のように書き方が変わる*/}
<div>square of count2: {square}</div>
<button onClick={increment1}>increment1</button>
<button onClick={increment2}>increment2</button>
</>
}
これでincrement1を押したとき、squareは走らなくなる
useRef
React hooksを基礎から理解する (useRef編) - Qiita
useRefは.currentによって現在の値を参照/変更できる状態管理オブジェクトを生成する。
vueでいうv-modelみたいなものに見える。
input要素の値を参照したり、逆に値を設定したりする時に使える。
code: index.js
import {useRef, useState} from 'react'
export default function UseMemo() {
const text, setText = useState("")
const inputEl = useRef(null)
const handleClick = () => { setText(inputEl.current.value) }
console.log("render")
return <>
<input ref={inputEl} type="text" />
<button onClick={handleClick}>set text</button>
<p>TEXT: {text}</p>
</>
}
useRefを使った入力フォームは、テキストボックスを書き換えてもこのコンポーネントの再描画が発生しない。
(handleClickするとtext stateの値が書き換わるため再描画が走る)
useRefの代わりにuseStateを使うと、テキストボックスを書き換える度にこのコンポーネントの再描画が発生してしまう
code: index.js
import {useState} from 'react'
export default function UseMemo() {
const text, setText = useState("")
const inputElement, setInputElement = useState("")
const handleClick = () => { setText(inputElement) }
console.log("描画!!")
return <>
<input value={inputElement} type="text"
onChange={(e) => setInputElement(e.target.value)} />
<button onClick={handleClick}>setText</button>
<p>TEXT: {text}</p>
</>
}
↓ React.memo, useCallback, useMemoを使ったパフォーマンスチューニングのまとめ
【React】もっと速くなる!?パフォーマンス最適化に挑戦! - Qiita